package com.cnap.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.NioUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.cnap.config.FileConfig;
import com.cnap.db.entity.FileInfoPO;
import com.cnap.db.entity.PatchInfoPO;
import com.cnap.db.entity.ProgressPO;
import com.cnap.db.mapper.FileMapper;
import com.cnap.model.application.ApplicationStatusEnum;
import com.cnap.model.exception.JsonException;
import com.cnap.model.file.FileConstants;
import com.cnap.model.file.FileInitReq;
import com.cnap.model.file.MergeReq;
import com.cnap.model.file.PatchRes;
import com.cnap.model.file.UploadPatchReq;
import com.cnap.model.progress.ProgressConstants;
import com.cnap.service.ApplicationService;
import com.cnap.service.FileService;
import com.cnap.service.MicroSvcService;
import com.cnap.service.ProgressService;
import com.cnap.utils.TokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

/**
 * <类文件描述>
 *
 * @author xc
 **/
@Service
public class FileServiceImpl implements FileService {
    private static final Logger LOGGER = LoggerFactory.getLogger(FileServiceImpl.class);
    private static final ExecutorService EXECUTOR_SERVICE = ThreadUtil.newExecutor(10, 20, 100);

    private final FileMapper fileMapper;
    private final FileConfig fileConfig;
    private final ProgressService progressService;

    @Autowired
    public FileServiceImpl(FileMapper fileMapper, FileConfig fileConfig, ProgressService progressService) {
        this.fileMapper = fileMapper;
        this.fileConfig = fileConfig;
        this.progressService = progressService;
    }

    /**
     * 初始化文件信息
     *
     * @param fileInitReq 请求
     * @return 文件id
     */
    @Override
    public String initFileInfo(FileInitReq fileInitReq) {
        // 转换po
        FileInfoPO fileInfoPO = fileInitReq.toPO();
        // 保存
        int result = fileMapper.saveFileInfo(fileInfoPO);
        if (result != 1) {
            LOGGER.error("save file info error, {}", fileInitReq);
            throw new JsonException(500, "内部错误");
        }
        return fileInfoPO.getId();
    }

    /**
     * 初始化文件信息(指定id)
     *
     * @param fileInitReq 请求
     * @param fileId      文件id
     * @return 文件id
     */
    @Override
    public String initFileInfo(FileInitReq fileInitReq, String fileId) {
        // 转换po
        FileInfoPO fileInfoPO = fileInitReq.toPO();
        if (CharSequenceUtil.isNotBlank(fileId)) {
            fileInfoPO.setId(fileId);
        }
        // 保存
        int result = fileMapper.saveFileInfo(fileInfoPO);
        if (result != 1) {
            LOGGER.error("save file info error, {}", fileInitReq);
            throw new JsonException(500, "内部错误");
        }
        return fileInfoPO.getId();
    }
        /*
        上传流程
        初始化上传，前端需要获取压缩文件总大小，文件哈希值以及业务相关信息，通过初始化接口上传信息，接口返回id
        根据总大小以及分片大小，将文件分成若干片
        分片文件上传，并带有步骤1返回的id，表明是这个文件的分片信息
        循环步骤3，直至所有分片成功上传
        发送合并请求，带有步骤1返回的id
         */

    /**
     * 上传分片信息
     *
     * @param uploadPatchReq 请求
     * @param multipartFile  分片文件
     * @return 分片响应
     */
    @Override
    public PatchRes uploadPatch(UploadPatchReq uploadPatchReq, MultipartFile multipartFile) {
        // 判断是否有先创建的文件信息
        FileInfoPO parentFileInfo = fileMapper.selectById(uploadPatchReq.getParentId());
        if (Objects.isNull(parentFileInfo)) {
            LOGGER.error("can't find parent file info, {}", uploadPatchReq);
            throw new JsonException(500, "无法获取父文件信息");
        }

        // 1. 已上传，直接返回
        PatchInfoPO oldPatch = fileMapper.selectByParentIdAndIndex(uploadPatchReq.getParentId(), uploadPatchReq.getIndex());
        String patchId;
        if (Objects.nonNull(oldPatch)) {
            // 如果在上传，则直接返回
            if (FileConstants.PATCH_STATUS_UPLOADING == oldPatch.getStatus()) {
                LOGGER.info("patch is uploading, {}", uploadPatchReq);
                return uploadPatchReq.toRes();
            }
            // 上传失败，重置状态
            if (FileConstants.PATCH_STATUS_FAILED == oldPatch.getStatus()) {
                LOGGER.warn("old patch upload catch error, try to reUpload");
                fileMapper.updatePatchInfoStatus(oldPatch.getId(), FileConstants.PATCH_STATUS_UPLOADING);
            }
            // 上传完成，直接返回
            if (FileConstants.PATCH_STATUS_FINISHED == oldPatch.getStatus()) {
                LOGGER.info("patch has been uploaded, {}", uploadPatchReq);
                return uploadPatchReq.toRes();
            }
            patchId = oldPatch.getId();
        } else {
            // 2.1 保存数据库
            PatchInfoPO patchInfoPO = uploadPatchReq.toPO();
            int result = fileMapper.savePatchInfo(patchInfoPO);
            if (result != 1) {
                LOGGER.error("save patch info error, {}", uploadPatchReq);
                throw new JsonException(500, "内部错误，请联系管理员");
            }
            patchId = patchInfoPO.getId();
        }
        // 2. 未上传
        // 校验分片文件名与初始文件名是否一致
        if (!uploadPatchReq.getFileName().equals(parentFileInfo.getFileName())) {
            LOGGER.error("patch file name={} is invalid, file info name={}", uploadPatchReq.getFileName(),
                    parentFileInfo.getFileName());
            throw new JsonException(500, "分片文件名错误，请校验后上传");
        }
        // 2.2 保存文件
        // 不能使用异步保存，因为multipartFile的临时文件在controller return之后就会被删除
        // 所以，要么使用同步的方法，要么先将multipartFile的临时文件保存下来
        // 此处将multipartFile保存至缓冲区，在进行保存，所以可以异步
        try {
            InputStream inputStream = multipartFile.getInputStream();
            // 缓冲
            BufferedInputStream bufferedInputStream = IoUtil.toBuffered(inputStream);
            // 异步保存
            CompletableFuture.runAsync(() -> saveFile(bufferedInputStream, uploadPatchReq, patchId), EXECUTOR_SERVICE);
        } catch (IOException e) {
            LOGGER.error("file save error, try to update patch info to fail, e={}, id={}, index={}", e.getMessage(),
                    patchId, uploadPatchReq.getIndex());
            fileMapper.updatePatchInfoStatus(patchId, FileConstants.PATCH_STATUS_FAILED);
        }
        // 转换响应
        return uploadPatchReq.toRes();
    }

    /**
     * 合并分片文件
     *
     * @param mergeReq 合并请求
     */
    @Override
    @Transactional
    public void mergePatchFile(MergeReq mergeReq) {
        // 将进度设置为50
        makePercentWithStageMerge(mergeReq.getParentId());

        FileInfoPO fileInfoPO = fileMapper.selectById(mergeReq.getParentId());
        // 1. 校验
        validate(mergeReq, fileInfoPO);
        // 2. 合并文件
        // 合并文件也需要进度
        String userName = TokenUtils.getUserName();
        // 由于是异步，所以threadLocal失效，需要手动将token传入
        CompletableFuture.runAsync(() -> mergePatchFile(mergeReq, fileInfoPO, userName), EXECUTOR_SERVICE);

    }

    private void makePercentWithStageMerge(String applicationId) {
        // 组装基本进度对象
        ProgressPO progressPO = new ProgressPO();
        progressPO.setId(applicationId);
        progressPO.setPercent(ProgressConstants.UPLOAD_PATCH_PERCENT);
        progressPO.setStage(ProgressConstants.STAGE_UPLOAD_MERGE);
        progressPO.setType(ProgressConstants.TYPE_UPLOAD);
        progressService.updateProgress(progressPO);
    }

    private void mergePatchFile(MergeReq mergeReq, FileInfoPO fileInfoPO, String userName) {
        int patchNums = fileInfoPO.getPatchNums();
        // 后缀，默认为zip
        String suffix = FileNameUtil.getSuffix(fileInfoPO.getFileName());
        String mergeFileName = mergeReq.getParentId() + "." + suffix;
        // 保存路径
        String savePath = getSavePath(mergeReq.getParentId());
        // 保存文件名，使用applicationId
        String mergeFilePath = savePath + File.separator + mergeFileName;
        if (FileUtil.exist(mergeFilePath)) {
            // 有之前保存的，则先删除
            LOGGER.warn("merge file={} has old version, try to delete", mergeFileName);
            FileUtil.del(mergeFilePath);
        }
        File mergeFile = new File(mergeFilePath);

        try (FileOutputStream outputStream = new FileOutputStream(mergeFile);
             FileChannel out = outputStream.getChannel()
        ) {
            // 每个分片加载完毕，会更新以下进度，到99时停止，然后计算MD5
            for (int i = 0; i < patchNums; i++) {
                String patchName = mergeReq.getParentId() + "_" + i;
                String patchPath = savePath + File.separator + patchName;
                File patchFile = new File(patchPath);
                // 分片写入
                try (FileInputStream inputStream = new FileInputStream(patchFile);
                     FileChannel in = inputStream.getChannel()
                ) {
                    in.transferTo(0, in.size(), out);
                }
                // 计算进度
                if (i < patchNums - 1) {
                    int percent = ProgressConstants.UPLOAD_PATCH_PERCENT + (ProgressConstants.UPLOAD_MERGE_PERCENT * (i + 1)) / patchNums;
                    progressService.updateProgressPercent(mergeReq.getParentId(), percent);
                } else {
                    // 最后一个时，单独更新为99
                    progressService.updateProgressPercent(mergeReq.getParentId(), 99);
                }
            }
            LOGGER.info("merge finish, start md5");
            // 结束后将MD5值更新进数据库
            String md5 = SecureUtil.md5(mergeFile);
            // 判断该用户是否已经上传同样的应用（MD5值）
            FileInfoPO existFileInfo = fileMapper.selectFileInfoByMd5AndUserName(md5, userName);
            if (Objects.isNull(existFileInfo)) {
                // 更新MD5
                fileMapper.updateFileMd5ById(mergeReq.getParentId(), md5);
                LOGGER.info("update file md5={}", md5);
                // 更新状态
                progressService.updateProgressPercent(mergeReq.getParentId(), 100);
                // 循环依赖
                ApplicationService applicationService = SpringUtil.getBean(ApplicationService.class);
                applicationService.updateApplicationStatus(mergeReq.getParentId(), ApplicationStatusEnum.UPLOADED_FILE.getValue());
                // 上传完成，自动拆解
                MicroSvcService microSvcService = SpringUtil.getBean(MicroSvcService.class);
                LOGGER.info("start split application automatically, appId={}", mergeReq.getParentId());
                microSvcService.splitMicroServices(mergeReq.getParentId());
            } else {
                // 已上传同样的文件
                LOGGER.warn("user {} has upload same zip file, md5={}", userName, md5);
                progressService.updateProgressException(mergeReq.getParentId(), "已上传同样的应用");
            }
        } catch (IOException e) {
            LOGGER.error("merge file error: {}", e.getMessage());
            progressService.updateProgressException(mergeReq.getParentId(), "文件出错，请重试");
        }
    }

    private void validate(MergeReq mergeReq, FileInfoPO fileInfoPO) {
        // 判断文件
        if (Objects.isNull(fileInfoPO)) {
            LOGGER.error("can't find file info, {}", mergeReq);
            progressService.updateProgressException(mergeReq.getParentId(), "无法获取父文件信息");
            return;
        }
        // 比较数据库中的大小与实际分片相加大小的值是否一直
        List<PatchInfoPO> patchInfoPOS = fileMapper.selectByParentId(mergeReq.getParentId());
        // 获取总大小
        long patchTotalSize = patchInfoPOS.stream().mapToLong(PatchInfoPO::getPatchSize).sum();
        // 比较大小
        if (fileInfoPO.getTotalSize() != patchTotalSize) {
            LOGGER.error("patch total size is invalid, file info total size={}, path total size={}",
                    fileInfoPO.getTotalSize(), patchTotalSize);
            progressService.updateProgressException(mergeReq.getParentId(), "文件损坏，请重新上传");
        }
        // TODO 此处判断是否使用实际文件数量比较好
        int patchNums = fileInfoPO.getPatchNums();
        // 判断文件数量
        if (patchInfoPOS.size() != patchNums) {
            LOGGER.error("patch nums is invalid, file info nums={}, patch nums={}", patchNums,
                    patchInfoPOS.size());
            progressService.updateProgressException(mergeReq.getParentId(), "文件损坏，请重新上传");
        }
    }

    /**
     * 根据id删除文件信息和分片信息
     *
     * @param parentId 父文件id
     * @return 结果
     */
    @Override
    @Transactional
    public boolean deleteFileInfoAndPatchInfoById(String parentId) {
        // 删除文件
        int result = fileMapper.deleteFileInfoById(parentId);
        if (result != 1) {
            LOGGER.error("delete file info error, id={}", parentId);
            return false;
        }
        // 删除分片
        List<PatchInfoPO> patchInfoPOS = fileMapper.selectByParentId(parentId);
        result = fileMapper.deletePatchByParentId(parentId);
        if (result != patchInfoPOS.size()) {
            LOGGER.error("delete patch info error, parentId={}", parentId);
            return false;
        }
        return true;
    }

    /**
     * 获取保存分片文件的地址
     *
     * @param applicationId 应用id（父文件id）
     * @return 地址
     */
    @Override
    public String getSavePath(String applicationId) {
        // 默认存储在/var/file/下
        return fileConfig.getPath() + File.separator + applicationId;
    }

    private void saveFile(BufferedInputStream inputStream, UploadPatchReq patchReq, String patchId) {
        // 带索引保存分片
        String newFileName = patchReq.getParentId() + "_" + patchReq.getIndex();
        // 创建保存的文件夹
        if (!FileUtil.exist(getSavePath(patchReq.getParentId()))) {
            FileUtil.mkdir(getSavePath(patchReq.getParentId()));
        }
        // 创建文件
        String filePath = getSavePath(patchReq.getParentId()) + File.separator + newFileName;
        if (FileUtil.exist(filePath)) {
            LOGGER.warn("patch={} has old version, try to delete", newFileName);
            FileUtil.del(filePath);
        }

        // 缓冲区文件
        BufferedOutputStream bufferedOutputStream = FileUtil.getOutputStream(filePath);
        IoUtil.copy(inputStream, bufferedOutputStream, NioUtil.DEFAULT_BUFFER_SIZE);
        // 更新状态
        fileMapper.updatePatchInfoStatus(patchId, FileConstants.PATCH_STATUS_FINISHED);
        LOGGER.info("save patch={}", newFileName);
        // 计算并更新进度
        progressService.calPercentWithOnePatchAndUpdate(patchReq.getParentId());
    }

}
